package jmine.tec.web.servlet;

import java.io.File;
import java.io.IOException;
import java.io.InputStream;
import java.net.URISyntaxException;
import java.net.URL;
import java.nio.charset.Charset;

import javax.servlet.ServletConfig;
import javax.servlet.ServletException;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServlet;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

import jmine.tec.utils.io.IOUtils;
import bancosys.tec.utils.md5sum.MD5SumTool;

/**
 * {@link HttpServlet} que responde ao get devolvendo um arquivo no classpath como conteudo. Manda e verifica a ETag vinda do cliente para
 * get condicional (If-None-Match)
 * 
 * @author takeshi
 */
public class ClasspathResourceServlet extends HttpServlet {

    private String resource;

    private State lastReturned;

    private String charset;

    /**
     * {@inheritDoc}
     */
    @Override
    public void init(ServletConfig config) throws ServletException {
        super.init(config);
        this.resource = config.getInitParameter("resource");
        this.charset = config.getInitParameter("charset");
        try {
            getCurrentState();
        } catch (IOException e) {
            throw new ServletException(e);
        }
        if (charset == null) {
            charset = Charset.defaultCharset().name();
        }
    }

    /**
     * {@inheritDoc}
     */
    @Override
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        State now = getCurrentState();
        if (now.data == null) {
            resp.sendError(HttpServletResponse.SC_NOT_FOUND);
        }
        String etag = req.getHeader("If-None-Match");
        if (now.etag != null) {
            resp.setHeader("ETag", now.etag);
        }
        if (etag != null && etag.equals(now.etag)) {
            resp.setStatus(HttpServletResponse.SC_NOT_MODIFIED);
        } else {
            doWriteData(resp, now);
        }
    }

    /**
     * Escreve os dados no response
     * 
     * @param resp {@link HttpServletResponse}
     * @param now State
     * @throws IOException e
     */
    private void doWriteData(HttpServletResponse resp, State now) throws IOException {
        resp.setContentType(this.getServletContext().getMimeType(this.resource) + "; charset=" + this.charset);
        resp.setContentLength(now.data.length);
        ServletOutputStream outputStream = resp.getOutputStream();
        outputStream.write(now.data);
        outputStream.flush();
    }

    /**
     * Devolve o stado novo, se houver uma modificacao no recurso.
     * 
     * @return {@link State}
     * @throws IOException e
     */
    private State getCurrentState() throws IOException {
        if (lastReturned == null) {
            lastReturned = new State(resource);
        }
        return lastReturned.reloadIfNecessary();
    }

    /**
     * Guarda o estado de um resource
     * 
     * @author takeshi
     */
    private static class State {
        private final byte[] data;

        private final String etag;

        private final String resource;

        private final URL url;

        private final long lastModified;

        /**
         * Construtor inicial. Procura o recurso no classpath
         * 
         * @param resource String
         * @throws IOException e
         */
        public State(String resource) throws IOException {
            this.resource = resource;
            this.lastModified = calculateLastModified();
            this.url = getResourceURL(resource);
            if (url == null) {
                this.data = null;
                this.etag = null;
            } else {
                InputStream input = url.openStream();
                this.data = IOUtils.readFully(input);
                this.etag = calculateETag(data);
            }
        }

        /**
         * C'tor privado
         * 
         * @param resource String - nao sera usado
         * @param lastModified long
         * @param url URL
         * @throws IOException e
         */
        private State(String resource, long lastModified, URL url) throws IOException {
            this.resource = resource;
            this.lastModified = lastModified;
            this.url = url;
            InputStream input = url.openStream();
            this.data = IOUtils.readFully(input);
            this.etag = calculateETag(data);
        }

        /**
         * Devolve o recurso, procurando no classpath
         * 
         * @param resource {@link String}
         * @return {@link URL}
         */
        private static URL getResourceURL(String resource) {
            URL url = Thread.currentThread().getContextClassLoader().getResource(resource);
            if (url == null) {
                return ClasspathResourceServlet.class.getResource(resource);
            }
            return url;
        }

        /**
         * Calcula o ETag, usando md5
         * 
         * @param data byte array
         * @return String
         * @throws IOException e
         */
        private static String calculateETag(byte[] data) throws IOException {
            return MD5SumTool.md5Sum(data);
        }

        /**
         * Recarrega o estado se necessario
         * 
         * @return {@link State}
         * @throws IOException e
         */
        public State reloadIfNecessary() throws IOException {
            long newLastModified = calculateLastModified();
            if (this.lastModified == newLastModified) {
                return this;
            }
            return new State(this.resource, newLastModified, url);
        }

        /**
         * @return long
         */
        public long calculateLastModified() {
            if (url != null && "file".equals(url.getProtocol())) {
                try {
                    File theFile = new File(url.toURI());
                    return theFile.lastModified();
                } catch (URISyntaxException e) {
                    return 0;
                }
            }
            return 0;
        }

    }

}
